/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.stetho.inspector.network;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import com.facebook.stetho.common.ExceptionUtil;
import com.facebook.stetho.common.Util;

import javax.annotation.Nullable;

/**
 * Abstract class for pretty printer factory that asynchronously downloads schema needed for
 * pretty printing the payload
 */
public abstract class DownloadingAsyncPrettyPrinterFactory implements AsyncPrettyPrinterFactory {

  @Override
  public AsyncPrettyPrinter getInstance(final String headerName, final String headerValue) {

    final MatchResult result = matchAndParseHeader(headerName, headerValue);
    if (result == null) {
      return null;
    }
    String uri = result.getSchemaUri();
    URL schemaURL = parseURL(uri);
    if (schemaURL == null) {
      return getErrorAsyncPrettyPrinter(headerName, headerValue);
    } else {
      ExecutorService  executorService = AsyncPrettyPrinterExecutorHolder.getExecutorService();
      if (executorService == null) {
        //last peer is unregistered...
        return null;
      }
      final Future<String> response = executorService.submit(new Request(schemaURL));
      return new AsyncPrettyPrinter() {
        public void printTo(PrintWriter output, InputStream payload)
            throws IOException {
          try {
            String schema;
            try {
              schema = response.get();
            } catch (ExecutionException e) {
              Throwable cause = e.getCause();
              if (IOException.class.isInstance(cause)) {
                doErrorPrint(
                    output,
                    payload,
                    "Cannot successfully download schema: "
                        + e.getMessage());
                return;
              } else {
                throw e;
              }
            }
            doPrint(output, payload, schema);
          } catch (InterruptedException e) {
            doErrorPrint(
                output,
                payload,
                "Encountered spurious interrupt while downloading schema for pretty printing: "
                    + e.getMessage());
          } catch (ExecutionException e) {
            Throwable cause = e.getCause();
            throw ExceptionUtil.propagate(cause);
          }
        }

        public PrettyPrinterDisplayType getPrettifiedType() {
          return result.getDisplayType();
        }
      };
    }
  }

  /**
   * Match the correct header that contains information about the schema uri
   * @param headerName header name of a response that needs to be pretty printed
   * @param headerValue header value which contains the URI for
   * the schema data needed to pretty print the response body
   * @return MatchResult that has the schema uri and the type of prettified result. Null
   * if there is no correct header match.
   */
  @Nullable
  protected abstract MatchResult matchAndParseHeader(String headerName, String headerValue);

  /**
   * Note that the IOException thrown by this method will be propagated all the way up and
   * yield an error to the chrome devtools
   */
  protected abstract void doPrint(PrintWriter output, InputStream payload, String schema)
      throws IOException;

  @Nullable
  private static URL parseURL(String uri) {
    try {
      return new URL(uri);
    } catch (MalformedURLException e) {
      return null;
    }
  }

  private static void doErrorPrint(PrintWriter output, InputStream payload, String errorMessage)
      throws IOException {
    output.print(errorMessage + "\n" + Util.readAsUTF8(payload));
  }

  private static AsyncPrettyPrinter getErrorAsyncPrettyPrinter(
      final String headerName,
      final String headerValue) {
    return new AsyncPrettyPrinter() {
      @Override
      public void printTo(PrintWriter output, InputStream payload) throws IOException {
        String errorMessage = "[Failed to parse header: "
            + headerName + " : " + headerValue + " ]";
        doErrorPrint(output, payload, errorMessage);
      }

      @Override
      public PrettyPrinterDisplayType getPrettifiedType() {
        return PrettyPrinterDisplayType.TEXT;
      }
    };
  }


  protected class MatchResult {
    private final String mSchemaUri;
    private final PrettyPrinterDisplayType mDisplayType;

    public MatchResult(String schemaUri, PrettyPrinterDisplayType displayType) {
      mSchemaUri = schemaUri;
      mDisplayType = displayType;
    }

    public String getSchemaUri() {
      return mSchemaUri;
    }

    public PrettyPrinterDisplayType getDisplayType() {
      return mDisplayType;
    }
  }

  private static class Request implements Callable<String> {
    private URL url;

    public Request(URL url) {
      this.url = url;
    }

    @Override
    public String call() throws IOException {
      HttpURLConnection connection = (HttpURLConnection)url.openConnection();
      int statusCode = connection.getResponseCode();
      if (statusCode != 200) {
        throw new IOException("Got status code: " + statusCode + " while downloading " +
            "schema with url: " + url.toString());
      }
      InputStream urlStream = connection.getInputStream();
      try {
        return Util.readAsUTF8(urlStream);
      } finally {
        urlStream.close();
      }
    }
  }
}

